In [1]:
import warnings

import numpy as np
import pandas as pd

import plotly.express as px
from plotly.offline import init_notebook_mode
init_notebook_mode(connected=True)

from tqdm import tqdm

warnings.filterwarnings('ignore')

from concurrent.futures import ThreadPoolExecutor

Стратегия и функция полезности¶

Класс стратегии¶

Вначале мы задаем вспомогательный класс, представляющий собой открытую позицию, то есть записывает информацию об открытой позиции: размер позиции, цена и булево значение шорт или лонг позиция.

In [3]:
class MyPosition:
    
    def __init__(self, amount: float, price_current: float, short: bool) -> None:
        """
        Инициализация объекта MyPosition.

        :param amount: Количество активa.
        :param price_current: Текущая цена актива.
        :param acc_fees: Количесво накопленных комиссий.
        :param short: Флаг короткой позиции (True, если короткая; False, если длинная).
        """
        self._amount: float = amount
        self._price_current: float = price_current
        self._acc_fees: float = 0
        self._short: bool = short

    def update_state(self, price: float) -> None:
        """
        Обновление состояния позиции.

        :param price: Новая цена актива.
        """
        self._price_current = price
        if self._short:
            # Рассчитываем комиссию за перенос короткой позиции
            transfer_fee = abs(self._amount) * self._price_current * 0.00065
            self._acc_fees += transfer_fee

    def balance(self) -> float:
        """
        Вычисление баланса позиции.

        :return: Баланс позиции.
        """
        return self._amount * self._price_current

Теперь про класс стратегии:

Поля Класса Стратегии¶

_position: текущая открытая позиция (если есть).

_states: данные о состоянии рынка (временная метка, открытые позиции, цена).

_equity: баланс пользователя.

_margin_equity: сумма использованной маржи.

Основная Функция¶

Метод run выполняет основную стратегию. Происходит построение скользящей средней, установка диапазона, итерационное отслеживание состояния рынка, а также открытие и закрытие позиций при соответствующих условиях. В коце цикла происходит принудительное закрытие позиции.

Вспомогательные Функции¶

calc_upper_and_lower: расчет верхней и нижней границ скользящей средней.

open_short: открытие короткой позиции.

open_long: открытие длинной позиции.

close_short: закрытие короткой позиции.

close_long: закрытие длинной позиции.

Параметры¶

RISK: параметр риска.

STD_COUNT_UP, STD_COUNT_DOWN: коэффициенты для расчета границ скользящей средней.

MA_COUNT: ширина окна для построения скользящей средней.

Данные на выходе¶

Результат выполнения стратегии записывается в массив, содержащий информацию о времени, цене актива, балансе пользователя, типе позиции, границах скользящей средней и флагах покупки/продажи.

In [4]:
class Strategy:

    params = {'FEE': 0.0004}

    def __init__(self, states, start_equity):
        self._position = None
        self._states = states
        self._equity = start_equity
        self._margin_equity = 0

    def run(self, RISK, STD_COUNT_UP, STD_COUNT_DOWN, MA_COUNT):
        states_ma = self._states['pos'].rolling(window=MA_COUNT).mean()
        data = []

        for i in tqdm(range(len(self._states)), disable=True):
            if i < 10 * MA_COUNT:
                continue
            elif (i == 10 * MA_COUNT):
                state = self._states.loc[i]
                pos_ma = states_ma[i]
                prev_pos_ma = states_ma[i - 1]
                (pos_ma_upper, pos_ma_lower) = self.calc_upper_and_lower(i, states_ma, STD_COUNT_UP, STD_COUNT_DOWN)
            else:
                state = self._states.loc[i]
                pos_ma = states_ma[i]
                prev_pos_ma = states_ma[i - 1]

            b = 0
            s = 0

            if self._position:
                if state['price'] != self._states['price'][i - 1]:
                    self._position.update_state(state['price'])

                #if prev_pos_ma > pos_ma_lower and pos_ma < pos_ma_lower and self._position._short:
                if prev_pos_ma < pos_ma_upper and pos_ma > pos_ma_upper:
                    pos_ma_upper, pos_ma_lower = self.calc_upper_and_lower(i, states_ma, STD_COUNT_UP, STD_COUNT_DOWN)
                    if self._position._short:
                        b = 1
                #if prev_pos_ma < pos_ma_upper and pos_ma > pos_ma_upper and not self._position._short:
                if prev_pos_ma > pos_ma_lower and pos_ma < pos_ma_lower:
                    pos_ma_upper, pos_ma_lower = self.calc_upper_and_lower(i, states_ma, STD_COUNT_UP, STD_COUNT_DOWN)
                    if not self._position._short:
                        s = 1

                if i == (len(self._states) - 2) and self._position._short:
                    b = 1
                if i == (len(self._states) - 2) and not self._position._short:
                    s = 1
                    
                data.append([
                    state['datetime'],
                    state['price'],
                    self._equity - self._position._acc_fees,
                    self._position._short,
                    pos_ma_upper,
                    pos_ma_lower,
                    pos_ma,
                    b,
                    s
                ])

                if b:
                    self.close_short() 
                if s:
                    self.close_long()
            else:
                if prev_pos_ma < pos_ma_upper and pos_ma > pos_ma_upper:
                    pos_ma_upper, pos_ma_lower = self.calc_upper_and_lower(i, states_ma, STD_COUNT_UP, STD_COUNT_DOWN)
                    self.open_long(state, RISK)
                    b = 1
                if prev_pos_ma > pos_ma_lower and pos_ma < pos_ma_lower:
                    pos_ma_upper, pos_ma_lower = self.calc_upper_and_lower(i, states_ma, STD_COUNT_UP, STD_COUNT_DOWN)
                    self.open_short(state, RISK)
                    s = 1

                data.append([
                    state['datetime'],
                    state['price'],
                    self._equity,
                    0,
                    pos_ma_upper,
                    pos_ma_lower,
                    pos_ma,
                    b,
                    s
                ])

        return pd.DataFrame(data, columns=['datetime', 'price', 'equity', 'short', 'pos_ma_upper', 'pos_ma_lower', 'pos_ma', 'buy', 'sell'])

    def calc_upper_and_lower(self, i, states_ma, STD_COUNT_UP, STD_COUNT_DOWN):
        #mean_pos_ma = states_ma.head(i).mean()
        std_pos_ma = states_ma.head(i).std()
        pos_ma_upper = states_ma[i] + STD_COUNT_UP * std_pos_ma
        pos_ma_lower = states_ma[i] - STD_COUNT_DOWN * std_pos_ma
        return pos_ma_upper, pos_ma_lower

    def open_short(self, state, RISK):
        if self._position:
            raise Exception(f'Cannot open position, already have one {self._position}')

        self._margin_equity += (np.floor((1 - self.params['FEE']) * RISK * self._equity / state['price']) * state['price'] - self.params['FEE'] * self._equity)

        amount = (-1) * np.floor((1 - self.params['FEE']) * RISK * self._equity / state['price'])

        self._position = MyPosition(amount, state['price'], True)

    def open_long(self, state, RISK):
        if self._position:
            raise Exception(f'Cannot open position, already have one {self._position}')

        self._margin_equity -= (np.floor((1 - self.params['FEE']) * RISK * self._equity / state['price']) * state['price'] + self.params['FEE'] * RISK * self._equity)

        amount = np.floor((1 - self.params['FEE']) * RISK * self._equity / state['price'])

        self._position = MyPosition(amount, state['price'], False)

    def close_short(self):
        if self._position._short == 0:
            raise Exception(f'Cannot close short position, it is long')

        self._equity += (
                self._margin_equity + (1 - self.params['FEE']) * self._position.balance() - self._position._acc_fees)
        self._margin_equity = 0
        self._position = None

    def close_long(self):
        if self._position._short == 1:
            raise Exception(f'Cannot close long position, it is short')

        self._equity += (self._margin_equity + (1 - self.params['FEE']) * self._position.balance() - self._position._acc_fees)
        self._margin_equity = 0
        self._position = None

Вспомогательные функции¶

Очистка данных¶

Отделение необходимых данных из двух csv файлов. А также разделение их на тренировочную и тестовую часть.

In [5]:
def clear_data(ticker):
     # Pos

    df = pd.read_csv(f'{ticker}_full_date.csv', sep=",")
    df = df.sort_values(by=['ticker', 'tradedate', 'tradetime']).drop_duplicates().reset_index().drop('index', axis=1)
    df['datetime'] = pd.to_datetime(df['tradedate'] + ' ' + df['tradetime']) 
    df = df.drop(['tradedate', 'tradetime'], axis=1) 
    df = df[df['clgroup'] == 'YUR'].reset_index().drop('index', axis=1)
    df = df[['datetime', 'pos']]
    data_pos = df

    # Price

    df = pd.read_csv(f'{ticker}_full_date_price.csv', sep=",")
    df.reset_index(inplace=True)
    df.rename(columns={'TRADEDATE': 'datetime'}, inplace=True)
    df['datetime'] = pd.to_datetime(df['datetime'])
    df = df[df['BOARDID']=='TQBR']
    df = df[['datetime', 'WAPRICE']]
    df.rename(columns={'WAPRICE': 'price'}, inplace=True)
    data_price = df

    # All data

    price_dict = dict(zip(data_price['datetime'].dt.date, data_price['price']))

    data_pos['price'] = data_pos['datetime'].dt.date.map(price_dict)

    all_data = data_pos.sort_values('datetime').drop_duplicates().dropna().reset_index().drop('index', axis=1)

    all_data_copy = all_data.copy()

    all_data_copy['date'] = all_data_copy['datetime'].dt.date

    # For train

    train_data = all_data_copy[all_data_copy['date'] < pd.to_datetime('2022-02-20').date()].reset_index().drop('index', axis=1)

    # For test

    test_data = all_data_copy[all_data_copy['date'] > pd.to_datetime('2022-12-31').date()].reset_index().drop('index', axis=1)

    return train_data, test_data

Тренировка¶

Подбор параметров среди 700 сочетаний параметров с помощью функции полезности. Вначале находится её максимум, далее рассматриваются сочетания, где функция полезности не меньше (max_utility - 0.1). И среди них находится максимум уже по доходности.

In [6]:
def train(all_data_copy1: pd.DataFrame, ticker) -> tuple:
    std_counts_up = np.arange(0.1, 2, 0.2)
    std_counts_down = np.arange(0.1, 2, 0.2)
    ma_counts = np.arange(200, 2200, 300)

    radius = 2

    parameters = []
    final_total_balances = []

    with ThreadPoolExecutor(max_workers=4) as executor:
        futures = []

        for std_count_up in std_counts_up:
            for std_count_down in std_counts_down:
                for ma_count in ma_counts:
                    future = executor.submit(run_strategy, all_data_copy1, std_count_up, std_count_down, ma_count)
                    futures.append((std_count_up, std_count_down, ma_count, future))

        for std_count_up, std_count_down, ma_count, future in futures:
            final_total_balance = future.result()
            parameters.append((std_count_up, std_count_down, ma_count))
            final_total_balances.append(final_total_balance)

    data_parameters = pd.DataFrame(parameters, columns=['std_count_up', 'std_count_down', 'ma_count'])
    data_parameters['final_total_balance'] = final_total_balances
    data_parameters['utility'] = 0

    for index_std_count_up in range(len(std_counts_up)):
        for index_std_count_down in range(len(std_counts_down)):
            for index_ma_count in range(len(ma_counts)):
                target_index = index_std_count_up * len(std_counts_down) * len(ma_counts) + index_std_count_down * len(ma_counts) + index_ma_count
                data_parameters.at[target_index, 'utility'] = utility_assess(data_parameters, index_std_count_up, index_std_count_down, index_ma_count, len(std_counts_up), len(std_counts_down), len(ma_counts), radius)

    max_utility = max(data_parameters['utility'])
    max_final_total_balance = max(data_parameters.loc[data_parameters['utility'] > max_utility - 0.1]['final_total_balance'])
    corresponding_parameters = (data_parameters.loc[data_parameters['final_total_balance'] == max_final_total_balance]['std_count_up'].values[0],
                                data_parameters.loc[data_parameters['final_total_balance'] == max_final_total_balance]['std_count_down'].values[0],
                                data_parameters.loc[data_parameters['final_total_balance'] == max_final_total_balance]['ma_count'].values[0])
    corresponding_utility = data_parameters.loc[data_parameters['final_total_balance'] == max_final_total_balance]['utility'].values[0]

    print(f"Ticker: {ticker}")
    print(f"Maximum Utility: {max_utility}")
    print(f"Maximum Total Balance: {max_final_total_balance}")
    print(f"Corresponding Utility: {corresponding_utility}")
    print(f"Corresponding Parameters (std_count, std_timerange, ma_count): {corresponding_parameters}")
    return corresponding_parameters

Тест¶

In [7]:
def test(all_data_copy2: pd.DataFrame, std_count_up: float, std_count_down: float, ma_count: float) -> float:
    strategy = Strategy(all_data_copy2, 10000000)
    df = pd.DataFrame(strategy.run(1, std_count_up, std_count_down, ma_count))
    df['equity'] = df['equity'] / df['equity'].iloc[0]
    final_total_balance = df['equity'].iloc[-1]

    print(f'Test profit = {final_total_balance}')
    return final_total_balance

Для многопоточности¶

Функции для примения многопоточности в программе.

In [8]:
def process_ticker(ticker):
    train_data, test_data = clear_data(ticker)
    corresponding_parameters = train(train_data, ticker)
    test(test_data, *corresponding_parameters)

def run_strategy(all_data_copy, std_count_up, std_count_down, ma_count):
    strategy = Strategy(all_data_copy, 10000000)
    df = pd.DataFrame(strategy.run(1, std_count_up, std_count_down, ma_count))
    df['equity'] = df['equity'] / df['equity'].iloc[0]
    final_total_balance = df['equity'].iloc[-1]
    return final_total_balance

Функция полезности¶

Тут мы задаём некоторую функцию полезности и затем считаем ее для каждого параметра, в качестве радиуса мы берём 2, то есть по рассматриваем дисперсию окресности из 125 $((2*2+1)^3)$ точек.

In [9]:
def utility_fun(income, var):
    return income - 10 * var

def utility_assess(df_plotly, index_std_count_up, index_std_count_down, index_ma_count, len_std_counts_up, len_std_counts_down, len_ma_counts, radius):
    income = df_plotly.loc[
        (df_plotly['std_count_up'] == df_plotly['std_count_up'].iloc[index_std_count_up]) &
        (df_plotly['std_count_down'] == df_plotly['std_count_down'].iloc[index_std_count_down]) &
        (df_plotly['ma_count'] == df_plotly['ma_count'].iloc[index_ma_count])
    ]['final_total_balance'].values[0]

    index_std_count_up -= radius
    index_std_count_down -= radius
    index_ma_count -= radius

    array_income = np.empty((2 * radius + 1, 2 * radius + 1, 2 * radius + 1))

    for i in range(2 * radius + 1):
        for j in range(2 * radius + 1):
            for k in range(2 * radius + 1):
                index_i = index_std_count_up + i
                index_j = index_std_count_down + j
                index_k = index_ma_count + k

                if (
                    0 <= index_i < len_std_counts_up and
                    0 <= index_j < len_std_counts_down and
                    0 <= index_k < len_ma_counts
                ):
                    array_income[i, j, k] = df_plotly.loc[
                        (df_plotly['std_count_up'] == df_plotly['std_count_up'].iloc[index_i]) &
                        (df_plotly['std_count_down'] == df_plotly['std_count_down'].iloc[index_j]) &
                        (df_plotly['ma_count'] == df_plotly['ma_count'].iloc[index_k])
                    ]['final_total_balance'].values[0]

    std_of_income = np.std(array_income[~np.isnan(array_income)]) if np.sum(~np.isnan(array_income)) >= 2 else 0
    utility = utility_fun(income, std_of_income)
    return utility

Применение¶

Подбор оптимальных параметров и вывод доходности.

In [5]:
ticker_names=['sr', 'gz', 'lk', 'vb', 'rn', 'mn', 'af', 'al', 'sn', 'yn', 'tt', 'nm', 'hy', 'me', 'fv', 'gk', 'mg']

with ThreadPoolExecutor() as executor:
    executor.map(process_ticker, ticker_names)
Ticker: sn
Maximum Utility: 0.31788352359963856
Maximum Total Balance: 1.3445981059217469
Corresponding Utility: 0.25376568222276386
Corresponding Parameters (std_count, std_timerange, ma_count): (1.1000000000000003, 0.7000000000000001, 200)
Test profit = 1.3296202276732652
Ticker: tt
Maximum Utility: 0.7413407906539069
Maximum Total Balance: 1.0
Corresponding Utility: -0.24055437492245002
Corresponding Parameters (std_count, std_timerange, ma_count): (0.7000000000000001, 1.1000000000000003, 2000)
Test profit = 1.150119740559525
Ticker: mn
Maximum Utility: 0.7707334583417212
Maximum Total Balance: 1.0
Corresponding Utility: -2251.9078693800307
Corresponding Parameters (std_count, std_timerange, ma_count): (0.1, 1.9000000000000004, 2000)
Ticker: rn
Maximum Utility: 0.6261486753719142
Maximum Total Balance: 0.996677589936
Corresponding Utility: 0.5424869624285997
Corresponding Parameters (std_count, std_timerange, ma_count): (0.5000000000000001, 1.9000000000000004, 2000)
Test profit = 1.1509129037672001
Test profit = 0.87408150129375
Ticker: af
Maximum Utility: 0.32667065666462475
Maximum Total Balance: 0.9164965757988631
Corresponding Utility: 0.24412374914941215
Corresponding Parameters (std_count, std_timerange, ma_count): (1.5000000000000004, 1.3000000000000003, 1100)
Test profit = 1.340699591308288
Ticker: nm
Maximum Utility: 0.790710850315741
Maximum Total Balance: 1.0861413002256
Corresponding Utility: 0.762506749378346
Corresponding Parameters (std_count, std_timerange, ma_count): (1.5000000000000004, 0.1, 2000)
Test profit = 0.982291381394388
Ticker: al
Maximum Utility: 0.6961923382382422
Maximum Total Balance: 1.0301410478167294
Corresponding Utility: 0.6961923382382422
Corresponding Parameters (std_count, std_timerange, ma_count): (1.5000000000000004, 0.5000000000000001, 2000)
Test profit = 0.7632387064492832
Ticker: yn
Maximum Utility: 0.5645708943478455
Maximum Total Balance: 0.9158679463687522
Corresponding Utility: 0.5232823541471745
Corresponding Parameters (std_count, std_timerange, ma_count): (1.1000000000000003, 1.9000000000000004, 200)
Test profit = 1.1444019765163957
Ticker: lk
Maximum Utility: 0.44546966860032255
Maximum Total Balance: 1.018224037645488
Corresponding Utility: 0.44546966860032255
Corresponding Parameters (std_count, std_timerange, ma_count): (1.3000000000000003, 1.9000000000000004, 2000)
Test profit = 0.6038929560625
Ticker: sr
Maximum Utility: 0.5607966519020064
Maximum Total Balance: 1.1022781941188184
Corresponding Utility: 0.5463749703404512
Corresponding Parameters (std_count, std_timerange, ma_count): (0.9000000000000001, 0.5000000000000001, 1400)
Test profit = 1.0282202039989643
Ticker: gz
Maximum Utility: 0.1843111111429132
Maximum Total Balance: 0.7386922179780796
Corresponding Utility: 0.15246636744707198
Corresponding Parameters (std_count, std_timerange, ma_count): (0.5000000000000001, 0.9000000000000001, 200)
Test profit = 1.137889739611637
Ticker: vb
Maximum Utility: 1.1560153379556783
Maximum Total Balance: 0.7789406628697979
Corresponding Utility: 1.1560153379556783
Corresponding Parameters (std_count, std_timerange, ma_count): (0.5000000000000001, 1.9000000000000004, 2000)
Test profit = 1.0172080985527603
Ticker: gk
Maximum Utility: 0.8073026145896098
Maximum Total Balance: 1.02242012544
Corresponding Utility: 0.026212750664499462
Corresponding Parameters (std_count, std_timerange, ma_count): (0.9000000000000001, 0.30000000000000004, 2000)
Test profit = 0.8533278441854918
Ticker: hy
Maximum Utility: 0.7909200273841779
Maximum Total Balance: 1.0444722672864002
Corresponding Utility: 0.6018266950103128
Corresponding Parameters (std_count, std_timerange, ma_count): (1.3000000000000003, 0.1, 2000)
Ticker: me
Maximum Utility: 0.797714891742952
Maximum Total Balance: 1.2169686430581383
Corresponding Utility: 0.797714891742952
Corresponding Parameters (std_count, std_timerange, ma_count): (1.5000000000000004, 0.5000000000000001, 800)
Test profit = 1.0997172356807976
Test profit = 0.5230073084245509
Ticker: mg
Maximum Utility: 0.5389506079936247
Maximum Total Balance: 1.140050052375
Corresponding Utility: 0.511356770226375
Corresponding Parameters (std_count, std_timerange, ma_count): (1.5000000000000004, 0.1, 1700)
Test profit = 0.5830371836850659
Ticker: fv
Maximum Utility: 0.5785285242951834
Maximum Total Balance: 0.975891574840926
Corresponding Utility: 0.5785285242951834
Corresponding Parameters (std_count, std_timerange, ma_count): (0.9000000000000001, 0.9000000000000001, 200)
Test profit = 0.9166081184728172

Средняя доходность составлвляет 0,9704867481

Отладочная печать¶

Пример работы стратегии на оптимальных параметрах для акций ПАО "Сургутнефтегаз".

Первый график показывает построение диапазона для траектории количества позиций.

Второй - баланс пользователя.

На третьем изображены точки открытия/закрытия позиций. Если сначала зелёная, а затем красная, то это лонг. Если наоборот, то шорт.

In [10]:
train_data, test_data = clear_data('sn')

strategy = Strategy(test_data, 10000000)
df = pd.DataFrame(strategy.run(1, 1.1, 0.7, 200))

px.line(df, x='datetime', y=['pos_ma', 'pos_ma_upper', 'pos_ma_lower']).show()

px.line(df, x='datetime', y='equity').update_xaxes(type='category').show()

up = pd.DataFrame(columns=['datetime', 'price'])
down = pd.DataFrame(columns=['datetime', 'price'])
for i in range(len(df['price'])):
    if df['buy'][i]:
        up.loc[len(up)] = df.iloc[i]
    elif df['sell'][i]:
        down.loc[len(down)] = df.iloc[i]
fig = px.line(df, x='datetime', y='price', title='График цен по времени', labels={'datetime': 'Дата и время', 'price': 'Цена'})
fig.add_trace(px.scatter(up, x='datetime', y='price').update_traces(marker=dict(color='green')).data[0])
fig.add_trace(px.scatter(down, x='datetime', y='price').update_traces(marker=dict(color='red')).data[0])
fig.update_xaxes(type='category')
fig.show()

Визуализация функции полезности¶

In [11]:
import plotly.graph_objs as go
from plotly.subplots import make_subplots

train_data, test_data = clear_data('sn')

step_std_counts_up = 0.2
step_std_counts_down = 0.2
step_ma_counts = 300
std_counts_up = np.arange(0.1, 2, step_std_counts_up)
std_counts_down = np.arange(0.1, 2, step_std_counts_down)
ma_counts = np.arange(200, 2200, step_ma_counts)

parameters = []
final_total_balances = []

with ThreadPoolExecutor(max_workers=4) as executor:
    futures = []
    
    for std_count_up in std_counts_up:
        for std_count_down in std_counts_down:
            for ma_count in ma_counts:
                future = executor.submit(run_strategy, train_data, std_count_up, std_count_down, ma_count)
                futures.append((std_count_up, std_count_down, ma_count, future))

    for std_count_up, std_count_down, ma_count, future in futures:
        final_total_balance = future.result()
        parameters.append((std_count_up, std_count_down, ma_count))
        final_total_balances.append(final_total_balance)

# Преобразование в DataFrame для удобства работы с данными
df_plotly = pd.DataFrame(parameters, columns=['std_count_up', 'std_count_down', 'ma_count'])
df_plotly['final_total_balance'] = final_total_balances

# Создание трехмерного графика в Plotly
fig = make_subplots(rows=1, cols=1, specs=[[{'type': 'scatter3d'}]])

scatter = go.Scatter3d(
    x=df_plotly['std_count_up'],
    y=df_plotly['std_count_down'],
    z=df_plotly['ma_count'],
    mode='markers',
    marker=dict(
        size=4,
        color=df_plotly['final_total_balance'],
        colorscale='Viridis',
        colorbar=dict(title='Final Total Balance')
    )
)

fig.add_trace(scatter)

# Наименование осей
fig.update_layout(scene=dict(
                    xaxis_title='std_count_up',
                    yaxis_title='std_count_down',
                    zaxis_title='ma_count')
                 )

# Отображение графика
fig.show()

Видно, что максимальные значения находяться в (1.1, 0.1, 200), но соседние точки имеют намного меньше доходности. А значит она нам не подходит.

In [12]:
radius = 2

array_utility = []
for index_std_count_up in range(len(std_counts_up)):
    for index_std_count_down in range(len(std_counts_down)):
        for index_ma_count in range(len(ma_counts)):
            array_utility.append(utility_assess(df_plotly, index_std_count_up, index_std_count_down, index_ma_count, len(std_counts_up), len(std_counts_down), len(ma_counts), radius))

df_plotly['final_utility_balance'] = array_utility
print(np.max(df_plotly['final_total_balance']), np.max(array_utility))
1.6428116300105169 0.31788352359963856
In [13]:
import plotly.graph_objs as go
from plotly.subplots import make_subplots

# Создание трехмерного графика в Plotly
fig = make_subplots(rows=1, cols=1, specs=[[{'type': 'scatter3d'}]])

scatter = go.Scatter3d(
    x=df_plotly['std_count_up'],
    y=df_plotly['std_count_down'],
    z=df_plotly['ma_count'],
    mode='markers',
    marker=dict(
        size=4,
        color=df_plotly['final_utility_balance'],
        colorscale='Viridis',
        colorbar=dict(title='Final Total Balance')
    ),
    text=df_plotly.apply(lambda row: f' final_total_balance: {row["final_total_balance"]}final_utility_balance: {row["final_utility_balance"]}', axis=1)
)

fig.add_trace(scatter)

# Наименование осей
fig.update_layout(scene=dict(
                    xaxis_title='std_count_up',
                    yaxis_title='std_count_down',
                    zaxis_title='ma_count')
                 )

# Отображение графика
fig.show()

На этом графике видно, что около появляется область точек, где функция полезности имеет около максимальные значения. Это используется при подборе оптимальных параметров.